﻿/*
 * Created by SharpDevelop.
 * User: Corrado Mio
 * Date: 03/05/2012
 * Time: 10:46
 * 
 * Classi implementate:
 * 
 *      UdpConnect: estende System.Net.Sockets.UdpClient
 * 
 * ed aggiunge un po' di funzioni di utilitza':
 * 
 * 
 */
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

using CSharpExt.Threading;

namespace CSharpExt.Net.Sockets
{
    
    // ----------------------------------------------------------------------
    // UdpConnect
    // ----------------------------------------------------------------------
    
    /// <summary>
    /// Estende UdpClient ed aggiunge un po' di funzioni di utilita':
    /// 
    ///     - l'EndPoint a cui connetersi viene specificato come property
    ///     - l'interfaccia di rete da utilizzare nel caso di EndPoint multicast
    ///       viene specificato come property
    ///     - Connected e IsBound possono essere utilizzate anche se UdpConnect su
    ///       stata chiamata la Close()
    ///     - la Close() puo' essere chiamata in modo concorrente. Solo un thread
    ///       la puo' eseguire.
    ///     - NON si deve fare l'ovveride della 'Dispose(bool', ma della OnClose()
    ///       la Dispose(bool) si preoccupa di far si che un'unico thread possa
    ///       chiamare la OnClose()
    ///     - IsMulticas permette di sapere se EndPoint e' un indirizzo multicast
    ///     - ReceivedFrom permette di sapere l'IP dell'ultima Receive
    ///     - 
    /// </summary>
    public class UdpConnect : System.Net.Sockets.UdpClient
    {
        #region Protected Data
        
        protected IPEndPoint _endPoint;
        
        protected IPAddress _interface;
        
        protected bool _sendTo;
        
        protected Exception _exception;
        
        protected EndPoint _fromep = new IPEndPoint(0,0);
        
        protected Disposed _disposed = new Disposed();
        
        #endregion
        
        #region Constructor
        
        public UdpConnect()
        {
            
        }
        
        #endregion
        
        #region Properties
        
        /// <summary>
        /// EndPoint a cui collegarsi (puo' essere un IPEndPoint o un DnsEndPoint)
        /// </summary>
        public IPEndPoint EndPoint
        {
            get { return _endPoint;  }
            set { _endPoint = value; }
        }
        
        /// <summary>
        /// Interfaccia di rete da utilizzare in caso di connessione multicast
        /// </summary>
        public IPAddress NetworkInterface
        {
            set { _interface = value; }
        }
        
        
        // ------------------------------------------------------------------
        
        /// <summary>
        /// Timeout in spedizione impostato sulla Socket
        /// </summary>
        public int SendTimeout
        {
            set { Client.SendTimeout = value; }
            get { return Client.SendTimeout;  }
        }
        
        /// <summary>
        /// Timeout in ricezione impostato sulla Socket
        /// </summary>
        public int ReceiveTimeout
        {
            set { Client.ReceiveTimeout = value; }
            get { return Client.ReceiveTimeout;  }
        }
        
        
        public EndPoint LocalEndPoint
        {
            get { return Client.LocalEndPoint; }
        }
        
        public EndPoint RemoteEndPoint
        {
            get { return Connected ? Client.RemoteEndPoint : null; }
        }
        
        // ------------------------------------------------------------------
        
        /// <summary>
        /// Se e' Connected ad un EndPoint remoto
        /// 
        /// Nota: puo' essere utilizzato anche dopo la Close()
        /// </summary>
        public bool Connected
        {
            get { return Client != null && Client.Connected; }
        }
        
        /// <summary>
        /// Se e' Bound ad un EndPoint locale
        /// 
        /// Nota: puo' essere utilizzato anche dopo la Close()
        /// </summary>
        public bool IsBound
        {
            get { return Client != null && Client.IsBound; }
        }
        
        // int Ttl { get; set; }
        
        /// <summary>
        /// Se l'EndPoint indicato e' un indirizzo multicast
        /// 
        /// [224.0.0.0, 239.255.255.255]
        /// </summary>
        public bool IsMulticast
        {
            get 
            {
                if (_endPoint as IPEndPoint == null)
                    return false;
                else
                    return IsMulticastAddress(_endPoint.Address);
            }
        }
        
        /// <summary>
        /// EndPoint dell'ultima Receive
        /// </summary>
        public EndPoint ReceivedFrom
        {
            get { return _fromep; }
        }
        
        #endregion
        
        #region Operations
        
        /// <summary>
        /// Inizializza UdpClient per la ricezione
        /// </summary>
        public void Listen()
        {
            Listen(_endPoint, _interface);
            
            OnOpen();
        }
        
        /// <summary>
        /// Inizializza UdpClient per l'invio
        /// </summary>
        public void Connect()
        {
            Connect(_endPoint, _interface, Ttl);
            
            OnOpen();
        }
        
        // ------------------------------------------------------------------
        // L'implementazione standrad della Close segue i seguenti pattern:
        //
        //      Close() -> virtual Dispose(bool)
        //
        // Attualmente l'implementazione della Dispose(bool) e' stata modificata
        // per avere la possibilita' di chiamare piu' volte la Close() da thread 
        // separati, ma solo uno la esegue effettivamente.
        //
        // Le operazioni di chiusura DEVONO essere implementate nella OnClose(), 
        // poiche' Dispose(bool) ha il compito di assicurarsi che la OnClose()
        // non venga chiamata da piu' thread contemporaneamente
        
        /// <summary>
        /// NON fare l'ovverride.
        /// 
        /// Fare l'ovveride della OnClose()
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
            if (_disposed.CanDispose)
                OnClose();
        }
        
        protected virtual void OnOpen()
        {
            
        }
        
        protected virtual void OnClose()
        {
            base.Dispose(true);
        }
        
        #endregion
        
        #region Send/receive
        
        /// <summary>
        /// Attende la ricezione di un messaggio
        /// 
        /// Nota: se il Socket non connesso, non genera nessuna eccezione
        /// ma ritorna 0
        /// </summary>
        public int Receive(byte[] buffer, int offset, int count)
        {
            int received = 0;
            
            if (Connected || IsBound)
            {
                received = Client.ReceiveFrom(buffer, offset, count, SocketFlags.None, ref _fromep);
            }
            
            return received;
        }
        
        public int ReceiveFrom(byte[] buffer, int offset, int count, ref EndPoint endPoint)
        {
            int received = 0;
            
            if (Connected || IsBound)
            {
                received = Client.ReceiveFrom(buffer, offset, count, SocketFlags.None, ref endPoint);
            }
            
            return received;
        }
        
        
        public void Send(byte[] buffer, int offset, int count)
        {
            if (Connected || IsBound)
            {
                if (_sendTo)
                    Client.SendTo(buffer, offset, count, SocketFlags.None, _endPoint);
                else
                    Client.Send(buffer, offset, count, SocketFlags.None);   
            }
        }
        
        public void SendTo(byte[] buffer, int offset, int count, EndPoint endPoint)
        {
            if (Connected || IsBound)
            {
                Client.SendTo(buffer, offset, count, SocketFlags.None, endPoint);
            }
        }
        
        #endregion
        
        #region Implementation
        
        protected bool IsMulticastAddress(IPAddress address)
        {
            byte firstByte = address.GetAddressBytes()[0];
            return 224<= firstByte && firstByte <= 239;
        }
        
        protected void Listen(IPEndPoint endPoint, IPAddress netitfc)
        {
            Bind(endPoint, netitfc);
            if(IsMulticastAddress(endPoint.Address))
                JoinMulticastGroup(endPoint, netitfc);
        }
        
        protected void Connect(IPEndPoint endPoint, IPAddress netitfc, int ttl)
        {
            if (IsMulticastAddress(endPoint.Address))
                SetMulticastInterface(netitfc, ttl);
            Connect(endPoint, netitfc);
        }
        
        
        protected void Bind(IPEndPoint endPoint, IPAddress netitfc)
        {
            Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

            if (netitfc == null && IsMulticastAddress(endPoint.Address))
            {
                Client.Bind(new IPEndPoint(IPAddress.Any, endPoint.Port));
            }
            else if (netitfc == null )
            {
                Client.Bind(endPoint);
            }
            else
            {
                Client.Bind(new IPEndPoint(netitfc, endPoint.Port));
            }
        }
        
        protected void Connect(IPEndPoint endPoint, IPAddress netitfc)
        {
            if (netitfc == null)
            {
                Client.Connect(endPoint);
            }
            else 
            {
                _sendTo = true;
                
                Client.Connect(new IPEndPoint(netitfc, endPoint.Port));
            }
        }

        protected void JoinMulticastGroup(IPEndPoint endPoint, IPAddress netitfc)
        {
            if (netitfc == null)
            {
                base.JoinMulticastGroup(endPoint.Address);
            }
            else
            {
                base.JoinMulticastGroup(endPoint.Address, netitfc);
            }
        }
        
        protected void DropMulticastGroup()
        {
            base.DropMulticastGroup(_endPoint.Address);
        }
        
        protected void SetMulticastInterface(IPAddress netitfc, int ttl)
        {
            if (netitfc != null)
            {
                Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, netitfc.GetAddressBytes());
                Client.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, ttl);
            }
        }
        
        #endregion
    }
    
    public class UdpConnectEx : UdpConnect
    {
        #region Protected Data
        
        private Thread _receiveThread;
        
        private bool _opened;
        
        #endregion
        
        #region Constructor
        
        public UdpConnectEx()
        {
            
        }
        
        #endregion
        
        #region Properties
        
        public bool Opened
        {
            get { return _opened; }
        }
        
        // ------------------------------------------------------------------
        // E' possibile attivare una ricezione asincrona
        // Se la ricezione fallisce, il thread viene terminato ed il UdpReceiver 
        // viene chiuso (viene chiamata la 'Close()' ).
        // 
        // Se la ricezione fallisce per timeout, viene chiamata la
        //
        //  OnReceiveTimeout
        //
        
        /// <summary>
        /// Callback per la ricezione asincrona.
        /// 
        /// Se inizializzato, viene creato un thread per la ricezione
        /// 
        /// Nota: il buffer passato E' SEMPRE lo stesso
        /// </summary>
        public Action<byte[], int, int> OnReceived;
        
        public Action<UdpConnect, byte[], int, int, EndPoint> OnReceivedFrom;
        
        public Action<UdpConnect> OnReceivedTimeout;
        
        /// <summary>
        /// Chiamato nella ricezione asincrona in caso di eccezione.
        /// 
        /// Subito il client viene chiuso
        /// </summary>
        public Action<UdpConnect, Exception> OnException;
        
        #endregion
        
        #region Operations
        
        protected override void OnOpen()
        {
            _opened = true;
            
            if (OnReceived != null)
                OnReceivedFrom = onReceivedFrom;
            
            if(OnReceivedFrom != null)
                Start();

            base.OnOpen();
        }
        
        protected override void OnClose()
        {
            _opened = false;
            
            base.OnClose();
            
            Stop();
        }
        
        #endregion
        
        #region Implementation
        
        protected Thread CreateThread(ThreadStart/*Action*/ onReceive)
        {
            return new Thread(onReceive);
        }
        
        private void onReceivedFrom(UdpConnect client, byte[] buffer, int offset, int count, EndPoint rep)
        {
            OnReceived(buffer, offset, count);
        }
        
        // ------------------------------------------------------------------
        
        private void Start()
        {
            _receiveThread = CreateThread(receive);
            _receiveThread.Start();
        }
        
        private void Stop()
        {
            if (_receiveThread != null && !_receiveThread.Join(1000))
                _receiveThread.Abort();
            _receiveThread = null;
        }

        private void receive()
        {
            EndPoint rep = new IPEndPoint(0,0);
            byte[] buffer = new byte[65536];

            try
            {
                while (Opened)
                {
                    try
                    {
                        int readed = Client.ReceiveFrom(buffer, 0, 65536, SocketFlags.None, ref rep);
                        
                        OnReceivedFrom(this, buffer, 0, readed, rep);
                    }
                    catch (Exception e)
                    {
                        _exception = e;
                            
                        // "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond"
                        if (e.Message.IndexOf("not properly respond after a period of time") != -1)
                        {
                            if (OnReceivedTimeout != null)
                                OnReceivedTimeout(this);
                        }
                        else
                        {
                            if (OnException != null)
                                OnException(this, e);
                            
                            break;
                        }
                    }
                }                
            }
            finally
            {
                //_receiveThread = null;

                Close();
            }
        }
        
        // ------------------------------------------------------------------
        
        private void onException(Exception e)
        {
            _exception = e;
            
            if (OnException != null)
                OnException(this, e);
        }
        
        #endregion
    }
    
    // ----------------------------------------------------------------------
    // UdpReceiver
    // ----------------------------------------------------------------------
    
    public class UdpReceiver : UdpConnectEx
    {
        #region Constructor
        
        public UdpReceiver()
        {
            
        }
        
        #endregion
        
        #region Operations
        
        public void Open()
        {
            base.Listen();
        }
        
        #endregion
    }
    
    // ----------------------------------------------------------------------
    // UdpSender
    // ----------------------------------------------------------------------
    
    public class UdpSender : UdpConnectEx
    {
        #region Constructor
        
        public UdpSender()
        {
            
        }
        
        #endregion
        
        #region Operations
        
        public void Open()
        {
            base.Connect();
        }
        
        #endregion
    }
    
}
